package com.atguigu.gmall.seckill.biz.impl;

import com.atguigu.gmall.common.config.interceptor.UserAuthContext;
import com.atguigu.gmall.common.constant.RabbitConstant;
import com.atguigu.gmall.common.constant.RedisConstant;
import com.atguigu.gmall.common.execption.GmallException;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.DateUtil;
import com.atguigu.gmall.common.util.JSONs;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.feign.user.UserFeignClient;
import com.atguigu.gmall.model.cart.entity.UserAuth;
import com.atguigu.gmall.model.enums.OrderStatus;
import com.atguigu.gmall.model.enums.PaymentWay;
import com.atguigu.gmall.model.enums.ProcessStatus;
import com.atguigu.gmall.model.mq.to.seckill.SeckillMsg;
import com.atguigu.gmall.model.order.entity.OrderDetail;
import com.atguigu.gmall.model.order.entity.OrderInfo;
import com.atguigu.gmall.model.order.vo.OrderConfirmVo;
import com.atguigu.gmall.model.seckill.entity.SeckillGoods;
import com.atguigu.gmall.model.user.entity.UserAddress;
import com.atguigu.gmall.mq.service.RabbitService;
import com.atguigu.gmall.seckill.biz.SeckillBizService;
import com.atguigu.gmall.seckill.mapper.SeckillGoodsMapper;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SeckillBizServiceImpl implements SeckillBizService {
    @Autowired
    SeckillGoodsMapper seckillGoodsMapper;

    Map<Long, SeckillGoods> localCache = new ConcurrentHashMap<>();

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    UserAuthContext userAuthContext;

    @Autowired
    RabbitService rabbitService;

    @Autowired
    UserFeignClient userFeignClient;

    @Autowired

    //数据库查询
    public List<SeckillGoods> getSecKillGoodsFromDB() {
        return seckillGoodsMapper.getSecKillGoods();
    }


    @Override
    public List<SeckillGoods> getSecKillGoods() {
        //如果只允许同一个人在执行的任务，需要加分布式锁，得到锁的人执行，没得到的不执行
        // 分布式调度。分布式计算，任务分片，任务容错，需要专业的分布式任务框架
        //Elastic-job xxl-job：分布式调度场景
        //简单场景： spring自带的schedule即可


        //先查本地缓存，
        List<SeckillGoods> localCaches = localCache.values().stream().collect(Collectors.toList());
        //缓存有，直接返回
        if (localCaches != null && localCaches.size() > 0) {
            log.info("本地缓存命中");
            return localCaches;
        }
        log.info("本地缓存未命中");
        return getFromRedisAndSync();
    }

    @Nullable
    private List<SeckillGoods> getFromRedisAndSync() {
        //再查redis缓存
        String cacheKey = RedisConstant.REDIS_SECKILL_CACHE + DateUtil.formatDate(new Date());
        HashOperations<String, Object, Object> hash = stringRedisTemplate.opsForHash();


        if (stringRedisTemplate.hasKey(cacheKey)) {
            try {
                //当天没有缓存的情况
                if ("x".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(cacheKey))) {
                    return null;
                }
                return null;
            } catch (Exception e) {
                List<SeckillGoods> redisCache = hash.values(cacheKey).stream()
                        .map(item -> JSONs.stringToObj(item.toString(), SeckillGoods.class))
                        .collect(Collectors.toList());
                if (redisCache != null && redisCache.size() > 0) {
                    //redis有的话可以直接放入本地缓存
                    saveLocalCache(redisCache);
                    return redisCache;
                } else {
                    log.info("分布式缓存未命中...");
                    return null;
                }
            }
        } else {
            //redis没有这个key，说明定时任务都没有运行;
            List<SeckillGoods> secKillGoods = getSecKillGoodsFromDB();
            //同步
            syncCache(secKillGoods);
            return secKillGoods;
        }
    }

    @Override
    public void upSeckillGoods() {

        //先查询数据库
        List<SeckillGoods> secKillGoods = getSecKillGoodsFromDB();
        //同步
        syncCache(secKillGoods);


    }

    @Override
    public SeckillGoods getDetail(Long skuId) {
        SeckillGoods seckillGoods = localCache.get(skuId);
        if (seckillGoods != null) {
            return seckillGoods;
        }
        //查redis并放入缓存
        getFromRedisAndSync();
        seckillGoods = localCache.get(skuId);
        return seckillGoods;
    }

    @Override
    public String getSeckillSkuIdStr(Long skuId) {
        //前置校验，校验本地内存是否有库存
        SeckillGoods seckillGoods = localCache.get(skuId);
        if (seckillGoods.getStockCount() <= 0) {
            throw new GmallException(ResultCodeEnum.SECKILL_STOCK_NOT_ERROR);
        }
        //前置校验，校验该当前时间是否在该商品的秒杀时间内
        if (seckillGoods.getStartTime().after(new Date())) {
            //开始时间比当前时间晚，说明秒杀还没开始
            throw new GmallException(ResultCodeEnum.SECKILL_NO_START);
        }
        if (seckillGoods.getEndTime().before(new Date())) {
            //结束时间在当前时间之前，说明秒杀以及结束了
            throw new GmallException(ResultCodeEnum.SECKILL_END);
        }

        //根据时间+用户id+商品id生成秒杀码
        UserAuth userAuth = userAuthContext.getUserAuth();
        String seckillCode = getSeckillCode(userAuth.getUserId(), skuId);
        //redis放一份，防止别人知道生成规则，绕开生成秒杀码,保存一天，这里需要用setIfAbsent
        // ,防止每次生成秒杀码就设置为0，影响后面幂等性判断
        stringRedisTemplate.opsForValue().setIfAbsent(RedisConstant.REDIS_SECKILL_CODE + seckillCode, "0", 1, TimeUnit.DAYS);
        //前端返回一份
        return seckillCode;

    }

    @Override
    public ResultCodeEnum seckillOrder(Long skuId, String skuIdStr) {
        //前置校验
        //校验一：秒杀码校验
        UserAuth userAuth = userAuthContext.getUserAuth();
        String seckillCode = getSeckillCode(userAuth.getUserId(), skuId);
        if (!seckillCode.equals(skuIdStr)) {
            log.warn("秒杀码校验失败");
            return ResultCodeEnum.SECKILL_ILLEGAL;
        }
        //校验二：判断redis里面是否有这个秒杀码
        if (!stringRedisTemplate.hasKey(RedisConstant.REDIS_SECKILL_CODE + seckillCode)) {
            log.warn("秒杀码校验失败，redis没有这个key");
            return ResultCodeEnum.SECKILL_ILLEGAL;
        }
        //校验三，时间校验，只判断结束时间，因为秒杀码redis里面有，说明活动一定开始了。
        SeckillGoods seckillGoods = localCache.get(skuId);
        if (seckillGoods.getEndTime().before(new Date())) {
            log.warn("秒杀活动已经结束");
            return ResultCodeEnum.SECKILL_END;
        }
//        校验四：库存校验，判断库存是否小于等于0，如果小于等于0，返回无库存状态码
        Integer stockCount = seckillGoods.getStockCount();
        if (stockCount <= 0) {
            log.warn("已告罄");
            return ResultCodeEnum.SECKILL_FINISH;
        }
        //校验五，幂等性校验，之前生成秒杀码时放入redis的码值为0，在这里做一个原子增操作，
        // 如果大于1，说明该用户已经操作过了，直接返回前端排队中状态码
        Long increment = stringRedisTemplate.opsForValue().increment(RedisConstant.REDIS_SECKILL_CODE + seckillCode);
        if (increment > 1) {
            log.warn("秒杀请求已经发过了，去检查状态");
            return ResultCodeEnum.SECKILL_OK;
        }
        //能走到这里，说明前置校验都通过了
        seckillGoods.setStockCount(stockCount - 1);
        //内存能扣（限流，扣到0了后面的请求都是无库存的状态码），redis和数据库不一定能扣。
        // 因为redis、数据库扣库存太慢了，所以内存扣完，可以直接返回排队中，如果内存能扣，然后发送mq消息，让他在后台慢慢扣
        SeckillMsg seckillMsg = new SeckillMsg();
        seckillMsg.setSeckillCode(seckillCode);
        seckillMsg.setUserId(userAuth.getUserId());
        seckillMsg.setSkuId(skuId);
        rabbitService.sendMsg(RabbitConstant.EXCHANGE_SECKILL_EVENT,
                RabbitConstant.RK_SECKILL_ORDER_QUEUE, seckillMsg);
        log.info("秒杀排队成功，消息已经发送成功，正在扣库存");
        return ResultCodeEnum.SECKILL_OK;
    }

    @Override
    public void updateStock(SeckillMsg msg) {
        String seckillCode = msg.getSeckillCode();
        //先修改redis数量
        HashOperations<String, String, String> hash = stringRedisTemplate.opsForHash();
        String cacheKey = RedisConstant.REDIS_SECKILL_CACHE + DateUtil.formatDate(new Date());
        String filed = msg.getSkuId().toString();
        String json = hash.get(cacheKey, filed);
        SeckillGoods goods = JSONs.stringToObj(json, SeckillGoods.class);
        goods.setStockCount(goods.getStockCount() - 1);
        hash.put(cacheKey, filed, JSONs.objecttoString(goods));
        if (goods.getStockCount() < 0) {
            //说明数据库早就没了，直接在redis占位
            stringRedisTemplate.opsForValue().set(RedisConstant.REDIS_SECKILL_ORDER + seckillCode, "x");
            return;
        }
        //能走到这里，说明reids还有库存
        try {
            //数据库没有非负的字段，扣减失败会抛异常
            seckillGoodsMapper.updateStock(msg.getSkuId());
            OrderInfo orderInfo = buildPreOrderInfo(msg);
            //存到数据库
            stringRedisTemplate.opsForValue().set(RedisConstant.REDIS_SECKILL_ORDER + seckillCode,
                    JSONs.objecttoString(orderInfo));
        } catch (Exception e) {
            e.printStackTrace();
            log.warn("数据库扣减失败");
            //redis占位
            stringRedisTemplate.opsForValue().set(RedisConstant.REDIS_SECKILL_ORDER + seckillCode, "x");
        }

    }

    @Override
    public ResultCodeEnum checkOrder(Long skuId) {
        //先判断redis有没有预订单的key
        UserAuth userAuth = userAuthContext.getUserAuth();
        String seckillCode = getSeckillCode(userAuth.getUserId(), skuId);
        String key = RedisConstant.REDIS_SECKILL_ORDER + seckillCode;
        if (!stringRedisTemplate.hasKey(key)) {
            //如果不存在说明可能还没执行到，让他继续排队
            return ResultCodeEnum.SECKILL_OK;
        }
        //说明有这个key，再判断这个value是不是x占位符号，如果是，说明没有库存了
        String json = stringRedisTemplate.opsForValue().get(key);
        if ("x".equals(json)) {
            return ResultCodeEnum.SECKILL_FINISH;
        }

        //说明生成预订单了
        OrderInfo orderInfo = JSONs.stringToObj(json, OrderInfo.class);
        if (orderInfo.getId() == null) {
            //说明生成预定单了还没下单
            return ResultCodeEnum.SECKILL_SUCCESS;
        }
        //能走到这里说明有下单了还没有支付
        return ResultCodeEnum.SECKILL_ORDER_SUCCESS;

    }

    @Override
    public OrderConfirmVo geConfireOrder(String skuIdStr) {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();


        List<UserAddress> userAddressList = userFeignClient.getUserAddressList().getData();
        orderConfirmVo.setUserAddressList(userAddressList);


        String json = stringRedisTemplate.opsForValue().get(RedisConstant.REDIS_SECKILL_ORDER + skuIdStr);
        OrderInfo orderInfo = JSONs.stringToObj(json, OrderInfo.class);
        List<OrderConfirmVo.Detail> collect = orderInfo.getOrderDetailList().stream()
                .map(item -> {
                    OrderConfirmVo.Detail detail = new OrderConfirmVo.Detail();
                    detail.setSkuId(item.getSkuId());
                    detail.setImgUrl(item.getImgUrl());
                    detail.setSkuName(item.getSkuName());
                    detail.setOrderPrice(item.getOrderPrice());
                    detail.setSkuNum(item.getSkuNum());
                    detail.setHasStock(1);
                    return detail;
                }).collect(Collectors.toList());

        orderConfirmVo.setDetailArrayList(collect);
        String tradeNo = "ATGUIGU_" + System.currentTimeMillis() + "_" + userAuthContext.getUserAuth().getUserId();
        orderConfirmVo.setTradeNo(tradeNo);
        BigDecimal totalAmount = collect.stream()
                .map(item -> item.getOrderPrice().multiply(new BigDecimal(item.getSkuNum())))
                .reduce(new BigDecimal("0"), BigDecimal::add); //聚合计算，两两相加
        orderConfirmVo.setTotalAmount(totalAmount);

        Integer totalNum = collect.stream()
                .map(OrderConfirmVo.Detail::getSkuNum)
                .reduce(0, (o1, o2) -> o1 + o2);
        orderConfirmVo.setTotalNum(totalNum);
        return orderConfirmVo;

    }

    private OrderInfo buildPreOrderInfo(SeckillMsg msg) {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setConsignee("");
        orderInfo.setConsigneeTel("");
        SeckillGoods detail = getDetail(msg.getSkuId());

        orderInfo.setTotalAmount(detail.getCostPrice());
        orderInfo.setOrderStatus(OrderStatus.UNPAID.name());
        orderInfo.setUserId(msg.getUserId());
        orderInfo.setPaymentWay(PaymentWay.ONLINE.name());
        orderInfo.setDeliveryAddress("");
        orderInfo.setOrderComment("");
        orderInfo.setOutTradeNo("");

        orderInfo.setTradeBody(detail.getSkuName());
        orderInfo.setCreateTime(new Date());
        long times = System.currentTimeMillis() + 30 * 60 * 1000;
        orderInfo.setExpireTime(new Date(times));
        orderInfo.setProcessStatus(ProcessStatus.UNPAID.name());
        orderInfo.setTrackingNo("");
        orderInfo.setParentOrderId(0L);
        orderInfo.setImgUrl(detail.getSkuDefaultImg());
        orderInfo.setProvinceId(0L);
        orderInfo.setOperateTime(new Date());
        orderInfo.setActivityReduceAmount(detail.getPrice().subtract(detail.getCostPrice()));
        orderInfo.setCouponAmount(new BigDecimal("0"));
        orderInfo.setOriginalTotalAmount(detail.getPrice());
        orderInfo.setFeightFee(new BigDecimal("0"));
        orderInfo.setRefundableTime(new Date());

        //准备OrderDetail数据
        OrderDetail orderDetail = new OrderDetail();
        orderDetail.setSkuId(msg.getSkuId());
        orderDetail.setSkuName(detail.getSkuName());
        orderDetail.setImgUrl(detail.getSkuDefaultImg());
        orderDetail.setOrderPrice(detail.getCostPrice());
        orderDetail.setSkuNum(1);
        orderDetail.setCreateTime(new Date());
        orderDetail.setSplitTotalAmount(detail.getCostPrice());
        orderDetail.setSplitActivityAmount(new BigDecimal("0"));
        orderDetail.setSplitCouponAmount(detail.getPrice().subtract(detail.getCostPrice()));
        orderDetail.setUserId(msg.getUserId());
        orderInfo.setOrderDetailList(Arrays.asList(orderDetail));
        return orderInfo;

    }

    private String getSeckillCode(Long userId, Long skuId) {
        String dateStr = DateUtil.formatDate(new Date());
        String encrypt = MD5.encrypt(dateStr + '_' + userId + '_' + skuId);
        return encrypt;
    }

    private void syncCache(List<SeckillGoods> secKillGoods) {
        String cacheKey = RedisConstant.REDIS_SECKILL_CACHE + DateUtil.formatDate(new Date());

        //内存存一份
        //先判断当天是否有数据
        if (secKillGoods == null) {
            log.info("今天没有需要秒杀的商品.....");
            stringRedisTemplate.opsForValue().set(cacheKey, "x", 1, TimeUnit.DAYS);
            return;
        }

        saveLocalCache(secKillGoods);
        log.info("本地缓存设置完成......");
        //在redis存一份
        HashOperations<String, String, String> hash = stringRedisTemplate.opsForHash();
        secKillGoods.forEach(item -> {
            hash.put(cacheKey, item.getSkuId().toString(), JSONs.objecttoString(item));
        });
        //设置过期时间
        stringRedisTemplate.expire(cacheKey, 1, TimeUnit.DAYS);
        log.info("二级缓存设置完成......");
    }

    private void saveLocalCache(List<SeckillGoods> secKillGoods) {
        secKillGoods.forEach(item -> {
            localCache.put(item.getSkuId(), item);
        });
    }
}
